
点光源是一个无限小的点,其照亮的范围是一个球体的光源,光的亮点随着光源的距离变大而逐渐变小,当超出照亮范围时亮度值为0,点光源发出的光线在某点的亮度值与该点到光源距离的平方成反比。本节我们将对管线进行点光源和聚光灯的支持。
8.1.1 其他光源类型的数据
1. 跟方向光一样,我们场景中支持的其它类型的光源数量也是有限制的,非定向光源的照射范围有限。通常对于任何给定帧,我们只能看到非定向光线的子集,因此我们可以支持的最大值适用于单帧,而非整个场景。如果在某个范围的光源数量比我们设置的最大数量多,则应该忽略掉多余的光源,Unity会根据重要性对可见光源列表进行排序,如果光源不发生变化,哪些光源被忽略掉是固定的。如果有相机的移动或其它改变,有可能导致曝光的情况,所以最大光源数应定的高一些,我们在Lighting脚本中定义非定向光源的数量最大值为64。
//定义其他类型光源的最大数量
const int maxOtherLightCount = 64;
2. 就像方向光一样,GPU需要知道场景中的光源数量,颜色以及光源位置,我们定义这三个属性的着色器标识ID,然后定义两个数组存储每个光源颜色和位置数据。
static int otherLightCountId = Shader.PropertyToID("_OtherLightCount");
static int otherLightColorsId = Shader.PropertyToID("_OtherLightColors");
static int otherLightPositionsId = Shader.PropertyToID("_OtherLightPositions");
//存储其它类型光源的颜色和位置数据
static Vector4[] otherLightColors = new Vector4[maxOtherLightCount];
static Vector4[] otherLightPositions = new Vector4[maxOtherLightCount];
3. 在SetupLights方法中追踪定向光和非定向光的计数,若光源数量大于0,将相关光源数据发送到GPU。
void SetupLights()
{
...
int dirLightCount = 0, otherLightCount = 0;
...
buffer.SetGlobalInt(dirLightCountId, dirLightCount);
if (dirLightCount > 0)
{
buffer.SetGlobalVectorArray(dirLightColorsId, dirLightColors);
buffer.SetGlobalVectorArray(dirLightDirectionsId, dirLightDirections);
buffer.SetGlobalVectorArray(dirLightShadowDataId, dirLightShadowData);
}
buffer.SetGlobalInt(otherLightCountId, otherLightCount);
if (otherLightCount > 0)
{
buffer.SetGlobalVectorArray(otherLightColorsId, otherLightColors);
buffer.SetGlobalVectorArray(otherLightPositionsId, otherLightPositions);
}
}
4. 接下来在Light.hlsl中定义一个代表最大光源数量的宏,并在_CustomLight缓冲区中声明其它光源类型的颜色和位置属性,然后定义一个GetOtherLightCount方法返回非定向光源的数量。
#define MAX_OTHER_LIGHT_COUNT 64
CBUFFER_START(_CustomLight)
...
//非定向光源的属性
int _OtherLightCount;
float4 _OtherLightColors[MAX_OTHER_LIGHT_COUNT];
float4 _OtherLightPositions[MAX_OTHER_LIGHT_COUNT];
CBUFFER_END
//获取非定向光源的数量
int GetOtherLightCount ()
{
return _OtherLightCount;
}
8.1.2 点光源的支持
1. 在Lighting脚本中定义一个SetupPointLight方法,将点光源的颜色和位置信息存储到数组。
//将点光源的颜色和位置信息存储到数组
void SetupPointLight(int index, ref VisibleLight visibleLight)
{
otherLightColors[index] = visibleLight.finalColor;
//位置信息在本地到世界的转换矩阵的最后一列
otherLightPositions[index] = visibleLight.localToWorldMatrix.GetColumn(3);
}
2. 调整SetupLights方法中的代码,使用switch语句区分光源的类型,在未到达最大光源数量之前根据光源类型进行数据的存储。
for (int i = 0; i < visibleLights.Length; i++)
{
VisibleLight visibleLight = visibleLights[i];
switch (visibleLight.lightType)
{
case LightType.Directional:
if (dirLightCount < maxDirLightCount)
{
SetupDirectionalLight(dirLightCount++, ref visibleLight);
}
break;
case LightType.Point:
if (otherLightCount < maxOtherLightCount)
{
SetupPointLight(otherLightCount++, ref visibleLight);
}
break;
}
}
3. 现在点光源数据已经传递到GPU了,在Light.hlsl中定义一个GetOtherLight方法获取指定索引非定向光源的颜色和方向的数据,现在不支持投影,所以阴影衰减值为1。
//获取指定索引的非定向光源数据
Light GetOtherLight (int index, Surface surfaceWS, ShadowData shadowData)
{
Light light;
light.color = _OtherLightColors[index].rgb;
float3 ray = _OtherLightPositions[index].xyz - surfaceWS.position;
light.direction = normalize(ray);
light.attenuation = 1.0;
return light;
}
4. 最后在Lighting.hlsl的GetLighting方法添加一个for循环,遍历所有非定向光源获取照明结果。
//根据物体的表面信息和灯光属性获取最终光照结果
float3 GetLighting(Surface surfaceWS, BRDF brdf, GI gi)
{
...
for (int j = 0; j < GetOtherLightCount(); j++)
{
Light light = GetOtherLight(j, surfaceWS, shadowData);
color += GetLighting(surfaceWS, brdf, light);
}
return color;
}

8.1.3 光照随距离衰减
现在我们场景中物体可以受点光源影响了,但是现在很亮,光照强度应随着距离而进行衰减,光照越远,亮度越低。应遵循公式:

其中i为光照强度,d为光照距离,这被称为反平方定律。下面是距离衰减曲线,我们可以知道离光源距离小于1的时候,可能会变得非常亮。

1. 在Light.hlsl的GetOtherLight方法中,利用1除以光照距离的平方应用距离衰减,另外要保证距离的平方值不为0,给它设置为一个很小的正值。
//获取某个索引的非定向光源属性
Light GetOtherLight (int index, Surface surfaceWS, ShadowData shadowData)
{
...
//光照强度随距离衰减
float distanceSqr = max(dot(ray, ray), 0.00001);
light.attenuation = 1.0 / distanceSqr;
return light;
}

8.1.4 限制光照范围
尽管现在点光源强度随着距离的增加衰减的很快,但理论上光照还影响着所有物体,尽管它通常无法明显感知出来,漫反射不明显,但镜面反射在更远的距离有时候仍然可见。
我们要让渲染变得更有效率,需要限制光照的最大范围,超过这个范围就将光照强度设为0。点光源包含在一个包围球中,球体由光源位置和范围而定,且球体边界的光照不应突然消失,而应该通过距离衰减平滑过渡。Unity的URP和烘焙系统使用了下面的公式来定义距离衰减曲线,其中r是光照的范围,我们也会使用这个公式。


1. 在Lighting脚本的SetupPointLight方法中,将光照范围的平方的倒数存储在光源位置的W分量中,存储计算好的值是为了减少着色器的计算量。
//将点光源的颜色和位置信息存储到数组
void SetupPointLight(int index, ref VisibleLight visibleLight)
{
otherLightColors[index] = visibleLight.finalColor;
//位置信息在本地到世界的转换矩阵的最后一列
Vector4 position = visibleLight.localToWorldMatrix.GetColumn(3);
//将光照范围的平方的倒数存储在光源位置的W分量中
position.w = 1f / Mathf.Max(visibleLight.range * visibleLight.range, 0.00001f);
otherLightPositions[index] = position;
}
2. 然后在Light.hlsl的GetOtherLight方法中套用公式计算,最终使得光照强度随着范围和距离进行衰减。
Light GetOtherLight (int index, Surface surfaceWS, ShadowData shadowData)
{
...
float distanceSqr = max(dot(ray, ray), 0.00001);
//套用公式计算随光照范围衰减
float rangeAttenuation = Square(saturate(1.0 - Square(distanceSqr * _OtherLightPositions[index].w)));
//光照强度随范围和距离衰减
light.attenuation = rangeAttenuation / distanceSqr;
return light;
}

聚光灯光源和点光源类似,它也有光源位置和作用范围,区别是照亮范围是一个圆锥体,光源位置在圆锥体的锥顶处。我们已经在管线中支持了点光源,接下来支持聚光灯。
8.2.1 光照方向
1. 聚光灯还有一个方向属性,在Lighting脚本中定义非定向光源的光照方向着色器标识ID,和存储方向数据的数组。
static int otherLightDirectionsId = Shader.PropertyToID("_OtherLightDirections");
static Vector4[] otherLightDirections = new Vector4[maxOtherLightCount];
2. 在SetupLights方法中把方向数据传到GPU。
if (otherLightCount > 0)
{
...
buffer.SetGlobalVectorArray(otherLightDirectionsId, otherLightDirections);
}
3. 新建一个SetupSpotLight方法,内容和SetupPointLight方法差不多,这里通过转换矩阵的第三列并求反得到光源的光照方向并存储。
//将聚光灯光源的颜色、位置和方向信息存储到数组
void SetupSpotLight(int index, ref VisibleLight visibleLight)
{
otherLightColors[index] = visibleLight.finalColor;
Vector4 position = visibleLight.localToWorldMatrix.GetColumn(3);
position.w = 1f / Mathf.Max(visibleLight.range * visibleLight.range, 0.00001f);
otherLightPositions[index] = position;
//本地到世界的转换矩阵的第三列在求反得到光照方向
otherLightDirections[index] = -visibleLight.localToWorldMatrix.GetColumn(2);
}
4. 然后在SetupLights方法的switch分支判断中添加一个聚光灯case。
case LightType.Spot:
if (otherLightCount < maxOtherLightCount)
{
SetupSpotLight(otherLightCount++, ref visibleLight);
}
break;
5. 在Light.hlsl的_CustomLight缓冲区中声明一个_OtherLightDirections数组接收光照方向数据。
float4 _OtherLightDirections[MAX_OTHER_LIGHT_COUNT];
6. 最后在GetOtherLight方法中计算聚光灯的衰减值,先通过聚光灯方向和光照方向点积得到光照衰减,使得聚光角度在90度时达到0,照亮灯光前方的一切。
Light GetOtherLight (int index, Surface surfaceWS, ShadowData shadowData)
{
...
//得到聚光灯衰
float spotAttenuation = saturate(dot(_OtherLightDirections[index].xyz, light.direction));
//光照强度随范围和距离衰减
light.attenuation = spotAttenuation * rangeAttenuation / distanceSqr;
return light;
}

8.2.2 聚光角度
聚光灯有一个角度用来控制光锥的宽度,这个角度是从中间测量的,所以一个90度的角度看起来就像现在的一样。除此之外,还有一个单独的内角,控线光线以及何时开始衰减,URP和lightmapper通过在saturate之前对点积结果缩放和添加一些东西,然后对结果进行平方来做到这一点,公式如下:

其中d是点积结果,a和b为:


ri和ro是内角和外角弧度。

将上面的a和b带入公式可以得到:

1. 我们在Lighting脚本中可以计算公式中a和b的值,然后通过一个聚光角度数组将数据发送到着色器,首先定义其着色器标识ID和数组。
static int otherLightSpotAnglesId = Shader.PropertyToID("_OtherLightSpotAngles");
static Vector4[] otherLightSpotAngles = new Vector4[maxOtherLightCount];
2. 在SetupLights方法中把该数组传到GPU。
if (otherLightCount > 0)
{
...
buffer.SetGlobalVectorArray(otherLightSpotAnglesId, otherLightSpotAngles);
}
3. 在SetupSpotLight方法中计算公式中a和b的值,将它们存储到聚光角度数组的XY分量中,其中外角可以通过VisibleLight的spotAngle属性直接拿到,内角需要通过Light对象的innerSpotAngle属性拿到。
//将聚光灯光源的颜色、位置和方向、角度信息存储到数组
void SetupSpotLight(int index, ref VisibleLight visibleLight)
{
...
Light light = visibleLight.light;
float innerCos = Mathf.Cos(Mathf.Deg2Rad * 0.5f * light.innerSpotAngle);
float outerCos = Mathf.Cos(Mathf.Deg2Rad * 0.5f * visibleLight.spotAngle);
float angleRangeInv = 1f / Mathf.Max(innerCos - outerCos, 0.001f);
otherLightSpotAngles[index] = new Vector4(angleRangeInv, -outerCos * angleRangeInv);
}
4. 在Light.hlsl的_CustomLight缓冲区中声明该聚光角度数组。
float4 _OtherLightSpotAngles[MAX_OTHER_LIGHT_COUNT];
5. 在GetOtherLight方法中调整聚光衰减值的计算。
//获取某个索引的非定向光源属性
Light GetOtherLight (int index, Surface surfaceWS, ShadowData shadowData)
{
...
float4 spotAngles = _OtherLightSpotAngles[index];
//计算聚光灯衰减值
float spotAttenuation = Square(saturate(dot(_OtherLightDirections[index].xyz, light.direction) * spotAngles.x + spotAngles.y));
//光照强度随范围和距离衰减
light.attenuation = spotAttenuation * rangeAttenuation / distanceSqr;
return light;
}
6. 最后为了确保点光源不受到聚光角度衰减计算的影响,设置点光源数据时将聚光角度设置为0和1。
void SetupPointLight(int index, ref VisibleLight visibleLight)
{
...
otherLightSpotAngles[index] = new Vector4(0f, 1f);
}

8.2.3 配置内角
聚光灯始终可以配置外角角度,但在URP被引入之前是没有单独的内角的。所以灯光的Inspector面板中没有暴露内角角度,渲染管线可以通过覆盖灯光的Inspector面板来修改灯光,这是通过创建编辑器脚本来扩展LightEditor,且给它CustomEditorForRenderPipeline属性完成这个操作。该属性第一个参数必须是Light类型,第二个参数是我们希望覆盖Inspector面板的渲染管线资产类型。
1. 在CustomRP的Editor子文件夹下创建CustomLightEditor脚本,内容如下。
using UnityEngine;
using UnityEditor;
[CanEditMultipleObjects]
[CustomEditorForRenderPipeline(typeof(Light), typeof(CustomRenderPipelineAsset))]
public class CustomLightEditor : LightEditor
{
}
2. 要替换Inspector面板,首先重写OnInspectorGUI方法,我们需要做的额外操作就是首先检查是否仅选择了聚光灯,通过settings中的属性可以进行光源类型的判断,然后调用DrawInnerAndOuterSpotAngle方法绘制一个调节内外聚光角度滑块,最后调用ApplyModifiedProperties应用该滑块所做的修改即可。
//重写灯光Inspector面板
public override void OnInspectorGUI()
{
base.OnInspectorGUI();
if (!settings.lightType.hasMultipleDifferentValues &&(LightType)settings.lightType.enumValueIndex == LightType.Spot)
{
settings.DrawInnerAndOuterSpotAngle();
settings.ApplyModifiedProperties();
}
}

现在我们对非定向光源添加烘焙的支持。
8.3.1 烘焙光照
只需要将点光源和聚光灯的灯光组件Mode属性改为Baked,进行烘焙即可(若要烘焙阴影,修改Shadow Type选项)。然后会发现烘焙后光照比较亮,因为Unity默认使用了错误的灯光衰减,和旧版渲染管线的结果相匹配。

8.3.2 灯光委托
1. 我们可以告诉Unity使用不同的衰减,通过在Unity编辑器中执行光照烘焙之前提供一个委托方法。将CustomRenderPipeline改为内部类,然后在构造函数的末尾调用InitializeForEditor方法。
public partial class CustomRenderPipeline : RenderPipeline
public CustomRenderPipeline(bool useDynamicBatching, bool useGPUInstancing, bool useSRPBatcher, ShadowSettings shadowSettings)
{
...
InitializeForEditor();
}
2. 新建脚本CustomRenderPipeline.Editor,作为CustomRenderPipeline编辑模式的局部类,内容如下。
using Unity.Collections;
using UnityEngine;
using UnityEngine.Experimental.GlobalIllumination;
using LightType = UnityEngine.LightType;
public partial class CustomRenderPipeline
{
partial void InitializeForEditor();
}
3. 仅对于编辑器,需要重写lightmapper设置光照数据,通过提供一个委托方法,来传入一个Light数组。最后输出一个NativeArray<LightDataGI>结构委托的类型是 Lightmaing.RequestLightsDelegate,我们将使用lambda表达式定义该方法,因为在其它地方不需要它。
partial void InitializeForEditor();
#if UNITY_EDITOR
static Lightmapping.RequestLightsDelegate lightsDelegate =
(Light[] lights, NativeArray<LightDataGI> output) => { };
#endif
4. 我们必须为每个光配置一个LightDataGI结构,并将其添加到输出中。因为需要为每个光源类型使用不同的处理代码,所以添加switch语句。默认情况下调用光照数据的InitNoBake方法,传入光源的实例ID,指示Unity不要烘焙光照。
static Lightmapping.RequestLightsDelegate lightsDelegate =
(Light[] lights, NativeArray<LightDataGI> output) =>
{
var lightData = new LightDataGI();
for (int i = 0; i < lights.Length; i++)
{
Light light = lights[i];
switch (light.type)
{
default:
lightData.InitNoBake(light.GetInstanceID());
break;
}
output[i] = lightData;
}
};
5. 接下来根据不同的光源类型,创建一个专业的光源结构,调用LightmapperUtils的Extract方法,参数是光源和光源引用结构,然后调用光源数据的Init方法。现在对所有类型的光源执行此操作,我们目前不支持区域光源,如果存在,需要把强制把该光源的Mode属性设置为烘焙模式。
switch (light.type)
{
case LightType.Directional:
var directionalLight = new DirectionalLight();
LightmapperUtils.Extract(light, ref directionalLight);
lightData.Init(ref directionalLight);
break;
case LightType.Point:
var pointLight = new PointLight();
LightmapperUtils.Extract(light, ref pointLight);
lightData.Init(ref pointLight);
break;
case LightType.Spot:
var spotLight = new SpotLight();
LightmapperUtils.Extract(light, ref spotLight);
lightData.Init(ref spotLight);
break;
case LightType.Area:
var rectangleLight = new RectangleLight();
LightmapperUtils.Extract(light, ref rectangleLight);
rectangleLight.mode = LightMode.Baked;
lightData.Init(ref rectangleLight);
break;
default:
lightData.InitNoBake(light.GetInstanceID());
break;
}
6. 然后对所有的灯光数据的衰减类型设置为FalloffType.InverseSquared。
lightData.falloff = FalloffType.InverseSquared;
output[i] = lightData;
7. 现在要使Unity调用我们写好的委托,需要创建InitializeForEditor方法,其中调用Lightmapping.SetDelegate方法,把我们定义的委托作为参数传递过去。
#if UNITY_EDITOR
...
partial void InitializeForEditor()
{
Lightmapping.SetDelegate(lightsDelegate);
}
#endif
8. 当我们的渲染管线被处理时我们还需要清理和重置委托,通过重写Dispose方法,先进行清理,然后调用Lightmapping.ResetDelegate来重置委托。
//清理和重置委托
protected override void Dispose(bool disposing)
{
base.Dispose(disposing);
Lightmapping.ResetDelegate();
}
然后再烘焙一次场景,光照衰减就正确了。

8.3.3 阴影蒙版
把点光源和聚光灯的Mode设置为Mixed也能将阴影烘焙到ShadowMask中。每个光源都使用一个通道,就像方向光一样。但由于其范围有限,因此多个光源可以使用同一通道,只要它们不重叠。因此,阴影蒙版可以支持任意数量的光,但每个纹素最多只能支持四个。如果多个光源在尝试声明同一通道时重叠,那么最不重要的灯将强制设置为Baked模式,直到不再发生冲突。
1. 要将阴影蒙版用于点光源和聚光灯,先在Shadow脚本中定义一个ReserveOtherShadows方法,用于存储非定向光源的阴影数据。如果混合光源的模式为ShadowMask,只需要配置阴影强度和Mask通道。
//存储其他类型光源的阴影
public Vector4 ReserveOtherShadows(Light light, int visibleLightIndex)
{
if (light.shadows != LightShadows.None && light.shadowStrength > 0f)
{
LightBakingOutput lightBaking = light.bakingOutput;
if (lightBaking.lightmapBakeType == LightmapBakeType.Mixed && lightBaking.mixedLightingMode == MixedLightingMode.Shadowmask
)
{
useShadowMask = true;
return new Vector4(light.shadowStrength, 0f, 0f,lightBaking.occlusionMaskChannel );
}
}
return new Vector4(0f, 0f, 0f, -1f);
}
2. 在Lighting.cs中添加非定向光源的阴影数据的着色器标识ID和数组。
static int otherLightShadowDataId = Shader.PropertyToID("_OtherLightShadowData");
Vector4[] otherLightShadowData = new Vector4[maxOtherLightCount];
3. 在SetupLights方法中将该数组发送到GPU。
buffer.SetGlobalVectorArray(otherLightShadowDataId, otherLightShadowData);
4. 在SetupPointLight和SetupSpotLight方法中存储点光源和聚光灯的阴影数据。
void SetupPointLight (int index, ref VisibleLight visibleLight)
{
...
Light light = visibleLight.light;
otherLightShadowData[index] = shadows.ReserveOtherShadows(light, index);
}
void SetupSpotLight (int index, ref VisibleLight visibleLight)
{
...
otherLightShadowData[index] = shadows.ReserveOtherShadows(light, index);
}
5. 在Shadows.hlsl中定义一个代表非定向光源的阴影数据的结构体,和一个GetOtherShadowAttenuation方法,我们使用和方向光阴影相同的方法。如果阴影强度大于0,则总是调用GetBakedShadow方法,否则阴影衰减为1,表示没有阴影。
struct OtherShadowData
{
float strength;
int shadowMaskChannel;
};
//得到其他类型光源的阴影衰减
float GetOtherShadowAttenuation(OtherShadowData other, ShadowData global, Surface surfaceWS)
{
#if !defined(_RECEIVE_SHADOWS)
return 1.0;
#endif
float shadow;
if (other.strength > 0.0)
{
shadow = GetBakedShadow(global.shadowMask, other.shadowMaskChannel, other.strength);
}
else
{
shadow = 1.0;
}
return shadow;
}
6. 在Light.hlsl的_CustomLight缓冲区中声明非定向光源的阴影数据数组,定义一个GetOtherShadowData方法获取阴影强度和Mask通道,然后在GetOtherLight方法中计算非定向光源的光照衰减时乘以光源的阴影衰减。
CBUFFER_START(_CustomLight)
...
float4 _OtherLightShadowData[MAX_OTHER_LIGHT_COUNT];
CBUFFER_END
//获取其他类型光源的阴影数据
OtherShadowData GetOtherShadowData (int lightIndex)
{
OtherShadowData data;
data.strength = _OtherLightShadowData[lightIndex].x;
data.shadowMaskChannel = _OtherLightShadowData[lightIndex].w;
return data;
}
//获取某个索引的非定向光源属性
Light GetOtherLight (int index, Surface surfaceWS, ShadowData shadowData)
{
...
OtherShadowData otherShadowData = GetOtherShadowData(index);
//光照强度随范围和距离衰减
light.attenuation = GetOtherShadowAttenuation(otherShadowData, shadowData, surfaceWS) * spotAttenuation * rangeAttenuation / distanceSqr;
return light;
}
现在进行烘焙,点光源和聚光灯也有了烘焙阴影。

目前,所有可见光都会渲染对象的每个片元,这对于方向光来说很好,但非定向光源通常只影响该物体表面的一小部分片元,因此许多计算都是多余的,而且会影响渲染效率,为了支持许多性能良好的光源,我们需要使用一些手段减少每个片元的评估光源数量,有一个简单的办法就是使用Unity的逐对象光源索引。
其理念是由Unity确定哪些灯光会影响哪些对象,并将此信息发送到 GPU。然后在渲染每个对象时,只评估相关灯光,而忽略其它对象。因此灯光是根据每个对象而不是每个片元确定的。这通常适用于小对象,但不适合大型对象,因为如果光线只影响对象的一小部分,则它将评估其整个表面。此外,有多少灯光会影响每个对象是有限制的,因此较大的对象更容易缺少照明。
由于逐对象的光源索引不一定理想,可能会丢失一些照明,因此我们把它作为可配置选项,这样可以更方便地比较视觉效果和性能开销。
8.4.1 逐对象光源数据
1. 给CameraRenderer脚本的DrawVisibleGeometry方法添加一个bool参数以指示是否使用逐对象光源,如果为true则绘制设置时启用PerObjectData.LightData或PerObjectData.LightIndices。
void DrawVisibleGeometry(bool useDynamicBatching, bool useGPUInstancing, bool useLightsPerObject)
{
PerObjectData lightsPerObjectFlags = useLightsPerObject ? PerObjectData.LightData | PerObjectData.LightIndices : PerObjectData.None;
...
var drawingSettings = new DrawingSettings(unlitShaderTagId, sortingSettings)
{
//设置渲染时批处理的使用状态
enableDynamicBatching = useDynamicBatching,
enableInstancing = useGPUInstancing,
perObjectData = PerObjectData.Lightmaps | PerObjectData.ShadowMask | PerObjectData.LightProbe | PerObjectData.OcclusionProbe |
PerObjectData.LightProbeProxyVolume | PerObjectData.OcclusionProbeProxyVolume | PerObjectData.ReflectionProbes | lightsPerObjectFlags
};
...
}
2. 给Render方法添加相同的传参,用于调用DrawVisibleGeometry方法时传参。
public void Render(ScriptableRenderContext context, Camera camera,
bool useDynamicBatching, bool useGPUInstancing, bool useLightsPerObject,ShadowSettings shadowSettings)
{
...
//绘制几何体
DrawVisibleGeometry(useDynamicBatching, useGPUInstancing,useLightsPerObject);
...
}
3. 在CustomRenderPipeline脚本中定义该bool字段用于追踪。
bool useLightsPerObject;
public CustomRenderPipeline(bool useDynamicBatching, bool useGPUInstancing, bool useSRPBatcher, bool useLightsPerObject, ShadowSettings shadowSettings)
{
this.shadowSettings = shadowSettings;
//设置合批启用状态
this.useDynamicBatching = useDynamicBatching;
this.useGPUInstancing = useGPUInstancing;
this.useLightsPerObject = useLightsPerObject;
...
}
protected override void Render(ScriptableRenderContext context, Camera[] cameras)
{
foreach (Camera camera in cameras)
{
renderer.Render(context, camera, useDynamicBatching, useGPUInstancing, useLightsPerObject, shadowSettings);
}
}
4. 最后将该切换选项添加到CustomRenderPipelineAsset脚本中,默认为true,并在创建渲染管线实例时作为参数传递。
//是否使用逐对象光照
bool useLightsPerObject = true;
protected override RenderPipeline CreatePipeline()
{
return new CustomRenderPipeline(useDynamicBatching, useGPUInstancing, useSRPBatcher, useLightsPerObject, shadows);
}

8.4.2 清除光源索引
Unity会对每个对象创建一个活跃光源列表,这个列表包含了场景内所有存在的光源(无论该光源是否可见),并且包含方向光,然后根据光源的重要性进行了排序。下面我们清理这些灯光列表,只保留可见的非定向光源索引。
1. 在Lighting脚本的Setup方法中添加一个bool传参,以指示是否启用了逐对象光照,然后该参数传递给SetupLights方法。
public void Setup(ScriptableRenderContext context, CullingResults cullingResults,ShadowSettings shadowSettings, bool useLightsPerObject)
{
...
SetupLights(useLightsPerObject);
...
}
void SetupLights(bool useLightsPerObject){...}
2. 在CameraRenderer.Render方法中调用lighting.Setup方法时传递该bool值。
public void Render(ScriptableRenderContext context, Camera camera,
bool useDynamicBatching, bool useGPUInstancing, bool useLightsPerObject,ShadowSettings shadowSettings)
{
...
//光源数据和阴影数据发送到GPU计算光照
lighting.Setup(context, cullingResults, shadowSettings, useLightsPerObject);
...
}
3. 在SetupLights方法中循环可见光之前,从剔除结果中通过GetLightIndexMap方法拿到活跃的光源索引列表。这里要判断一下,如果未使用逐对象光源索引,则列表初始化为默认值,该值不会分配任何内容。
void SetupLights(bool useLightsPerObject)
{
//拿到光源索引列表
NativeArray<int> indexMap = useLightsPerObject ? cullingResults.GetLightIndexMap(Allocator.Temp) : default;
...
}
4. 在循环可见光的时候,只需要包含点光源和聚光灯的索引,所有其它类型的光源应跳过(将其索引设置为-1即可)。然后我们需要更改剩余的光源的索引以匹配我们的光源。
for (int i = 0; i < visibleLights.Length; i++)
{
int newIndex = -1;
VisibleLight visibleLight = visibleLights[i];
switch (visibleLight.lightType)
{
...
case LightType.Point:
if (otherLightCount < maxOtherLightCount)
{
newIndex = otherLightCount;
SetupPointLight(otherLightCount++, ref visibleLight);
}
break;
case LightType.Spot:
if (otherLightCount < maxOtherLightCount)
{
newIndex = otherLightCount;
SetupSpotLight(otherLightCount++, ref visibleLight);
}
break;
}
if (useLightsPerObject)
{
indexMap[i] = newIndex;
}
}
5. 然后还需要消除所有不可见光源的索引,这里添加一个for循环在第一个循环完成后执行此操作,完成后必须通过cullingResults调用SetLightIndexMap方法将调整后的光源索引列表发送回Unity,这时indexMap就不需要了,调用它的Dispose方法进行释放。
int i;
for (i = 0; i < visibleLights.Length; i++)
{
...
}
//消除所有不可见光的索引
if (useLightsPerObject)
{
for (; i < indexMap.Length; i++)
{
indexMap[i] = -1;
}
cullingResults.SetLightIndexMap(indexMap);
indexMap.Dispose();
}
6. 最后添加一个静态字符串字段,用于是否启用逐对象光源功能的关键字。
static string lightsPerObjectKeyword = "_LIGHTS_PER_OBJECT";
//消除所有不可见光的索引
if (useLightsPerObject)
{
...
indexMap.Dispose();
Shader.EnableKeyword(lightsPerObjectKeyword);
}
else
{
Shader.DisableKeyword(lightsPerObjectKeyword);
}
8.4.3 使用光源索引
1. 在Lit.shader的CustomLit Pass中声明该关键字。
//是否使用逐对象光源
#pragma multi_compile _ _LIGHTS_PER_OBJECT
2. 在UnityInput.hlsl的UnityPerDraw缓冲区中声明2个相关的属性,其中unity_LightData的Y分量中包含了灯光数量,unity_LightIndices的两个分量都包含一个光源索引,所以每个对象最多支持8个。
CBUFFER_START(UnityPerDraw)
...
real4 unity_LightData;
real4 unity_LightIndices[2];
#endif
3. 在Lighting.hlsl的GetLighting方法中判断_LIGHTS_PER_OBJECT关键字是否被定义。如果定义了,则使用unity_LightData的Y分量中的光源数量进行循环,且从unity_LightIndices中检索出合适的光源索引,可以通过将迭代器除以4和通过与4取模得到正确的向量(此时,着色器的编译器可能会提示整数除法和取模操作速度慢,可以通过将迭代器j转换成uint类型来忽略提示)。我们最多有8个光源索引可以使用,但Y分量中存储的灯光数量可能会超过该数值,我们在循环时对其进行一下限制。
float3 GetLighting(Surface surfaceWS, BRDF brdf, GI gi)
{
...
#if defined(_LIGHTS_PER_OBJECT)
for (int j = 0; j < min(unity_LightData.y, 8); j++)
{
int lightIndex = unity_LightIndices[(uint)j / 4][(uint)j % 4];
Light light = GetOtherLight(lightIndex, surfaceWS, shadowData);
color += GetLighting(surfaceWS, brdf, light);
}
#else
for (int j = 0; j < GetOtherLightCount(); j++)
{
Light light = GetOtherLight(j, surfaceWS, shadowData);
color += GetLighting(surfaceWS, brdf, light);
}
#endif
return color;
}

使用逐对象光源会使得GPU Instancing的批处理效率变低,因为只有灯光计数和光源索引列表相匹配的对象才会被分组,SRP Batcher并不受影响,每个对象仍然拥有自己优化后的Draw Call。
8|点光源和聚光灯
提交
暂无评论